package tabla_de_simbolos;


import analizadorlexico.Token;
import arbol_sintactico_abstracto.AST;
import java.util.*;
import generador_de_codigo.GeneradorDeCodigo;
import java.io.IOException;
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

/**
 *
 * @author Javi
 */
public class Clase extends Tipo{
    private HashMap<String, Metodo> tablaMetodos;
    private List<Atributo> listaAtributos;
    private Constructor constructor;
    private int offsetMet, offsetAt, linea;
    private Clase padre;
    private boolean pendiente;
    private boolean completa;

    public Clase(Token nombre, boolean pendiente) {
        this.nombre = nombre;
        this.pendiente = pendiente;
        offsetAt = 1;
        offsetMet = 0;
        listaAtributos = new ArrayList();
        tablaMetodos = new HashMap();
        constructor = null;
        padre = null;   
        completa = false;
        linea = nombre.getLinea();
    }    
   
    public HashMap<String, Metodo> getTablaMetodos() {
        return tablaMetodos;
    }

    public List<Atributo> getListaAtributos() {
        return listaAtributos;
    }

    public Constructor getConstructor() {
        return constructor;
    }

    public int getOffsetMet() {
        return offsetMet;
    }

    public int getOffsetAt() {
        return offsetAt;
    }

    public void insertarMetodo(String nombre , Metodo metodo) throws ErrorSemantico{
        if(getNombre().equals(nombre))
            throw new ErrorSemantico("Un metodo no puede llamarse igual que la clase. Linea: " + metodo.getLinea());
        if(tablaMetodos.containsKey(nombre))
            throw new ErrorSemantico("Ya existe un metodo con el mismo nombre. Linea: " + metodo.getLinea());
        tablaMetodos.put(nombre, metodo);
    }

    public void insertarAtributo(Token nombre, Tipo tipo) throws ErrorSemantico {
        if(getNombre().equals(nombre.getLexema()))
            throw new ErrorSemantico("Un atributo no puede llamarse igual que la clase. Linea: " + nombre.getLinea());
        for(Atributo atributo : listaAtributos){
            if(atributo.getNombre().equals(nombre.getLexema()))
                throw new ErrorSemantico("Ya existe una variable de instancia con el mismo nombre. Linea: " + nombre.getLinea());
        }
        listaAtributos.add(new Atributo(nombre, tipo,offsetAt));
    }

    public void setConstructor(Constructor constructor) {
        //chequear que el nombre sea el mismo que la clase
        this.constructor = constructor;
    }  
    
    public boolean pendiente(){
        return pendiente;
    }
    
    public void setPendiente(boolean b){
        pendiente = b;
    }

    public Clase getPadre() {
        return padre;
    }

    public void setPadre(Clase padre) {
        this.padre = padre;
        this.offsetAt = padre.getOffsetAt();
    }
    
    /**
     * Agrega los metodos del padre a la tabla de metodos de la clase y setea los offset de los metodos
     * @throws ErrorSemantico 
     */
    private void agregarMetodosHeredados() throws ErrorSemantico{
        if(!getNombre().equals("Object")){
            for (Map.Entry<String, Metodo> entry : padre.tablaMetodos.entrySet()) {
                //por cada metodo del padre:
                Metodo metPadre = entry.getValue();
                if(tablaMetodos.get(metPadre.getNombre()) != null){ //si el metodo esta en el padre
                    tablaMetodos.get(metPadre.getNombre()).setOffset(metPadre.getOffset());
                    chequearSignature(tablaMetodos.get(metPadre.getNombre()), metPadre);
                }
                else{
                    tablaMetodos.put(metPadre.getNombre(), metPadre);                    
                }
                if(offsetMet < metPadre.getOffset())
                        offsetMet = metPadre.getOffset();
            }
            //seteo offset de los metodos propios
            offsetMet++;
            for (Map.Entry<String, Metodo> entry : tablaMetodos.entrySet()) {
                if(!padre.tablaMetodos.containsKey(entry.getKey())){
                    Metodo propio = entry.getValue();    
                    if(propio.getModMetodo().getModificador().equals("dynamic")){
                        propio.setOffset(offsetMet); 
                        offsetMet++;
                    }
                }
            }
        }
        else
            offsetMet = 0;
    }
    
      /**
     * Setea los offsets de los atributos heredados
     * @throws ErrorSemantico 
     */
    private void agregarAtributosHeredados() throws ErrorSemantico{
        if(!getNombre().equals("Object")){
            offsetAt = padre.getOffsetAt();
            
            for(int i = 0 ; i < listaAtributos.size() ; i++){
                listaAtributos.get(i).setOffset(offsetAt);
                offsetAt++;
            }
        }
    }

    /**
     *  Comprueba que los encabezados de los metodos heredados se correspondan con los definidos en la clase
     * @param propio metodo de la clase
     * @param padre metodo de la clase padre
     * @throws ErrorSemantico 
     */
    private void chequearSignature(Metodo propio, Metodo padre) throws ErrorSemantico {
        //si los tipos de retorno no coinciden
        if(!propio.getTipoRetorno().getNombre().equals(padre.getTipoRetorno().getNombre()))
            throw new ErrorSemantico("Redefinicion del metodo '" + propio.getNombre() + "', tipos de retorno incompatibles. El tipo " + propio.getTipoRetorno().getNombre() + " no es compatible con " + padre.getTipoRetorno().getNombre() + ". Linea: " + propio.getLinea());
        //si los modificadore no coinciden
        if(!propio.getModMetodo().getModificador().equals(padre.getModMetodo().getModificador()))            
            throw new ErrorSemantico("Redefinicion del metodo '" + propio.getNombre() + "', los modificadores no se corresponden. Se esperaba " + padre.getModMetodo().getModificador() + " y se encontro " + propio.getModMetodo().getModificador() + ". Linea: " + propio.getLinea());
        //si no tienen la misma cantidad de parametros
        if(propio.getParametros().size() != padre.getParametros().size())
            throw new ErrorSemantico("Redefinicion del metodo '" + propio.getNombre() + "', la cantidad de parametros no coincide. Linea: " + propio.getLinea());
        
        for (int i = 0 ; i < propio.getParametros().size() ; i++) {
            if(!propio.getParametros().get(i).getTipo().getNombre().equals(padre.getParametros().get(i).getTipo().getNombre()))
                throw new ErrorSemantico("Redefinicion del metodo '" + propio.getNombre() + "', no coinciden los tipos de los parametros. Linea: " + propio.getLinea());            
        }                
    }
    
    
    public void chequeoDeclaraciones() throws ErrorSemantico, IOException{
        if(!completa){
            //chequeo que la clase se encuentre definida
            if(pendiente) 
                throw new ErrorSemantico("Clase " + getNombre() + " no definida. Linea: " + linea);  
            
            //chequeo que no exista un atributo con el nombre de un metodo
            for(Atributo atributo : listaAtributos){
                if(tablaMetodos.containsKey(atributo.getNombre()))
                    throw new ErrorSemantico("No puede haber un atributo y un metodo con el mismo nombre. Linea: " + atributo.getLinea());
            }               
            
            //si la clase no posee contructor creo uno vacio
            if(constructor == null){
                constructor = new Constructor(nombre,this);
                constructor.setAst(new AST(null, null));
            }
            
             //chequeo que no exista herencia circular
            if(!getNombre().equals("Object")){                    
                List<Clase> listPadres = new ArrayList();
                chequeoHerenciaCircular(this,listPadres);
            } 
            
            if(padre.isCompleta()){
                agregarMetodosHeredados();
                agregarAtributosHeredados();
            }
            else{
                padre.chequeoDeclaraciones();
                agregarMetodosHeredados();
                agregarAtributosHeredados();
            }     
             

            completa = true;
        }
        
        HashMap<Integer,Metodo> ordenados = new HashMap();
        
        GeneradorDeCodigo.generar("VT_"+ getNombre() + ": ");
        boolean sinMetodos = true;
        for (Map.Entry<String, Metodo> entry : tablaMetodos.entrySet()) {
            Metodo metodo = entry.getValue();
            ordenados.put(metodo.getOffset(), metodo);            
        }
        for(int i = 0 ; i <= ordenados.size() ; i++){
            Metodo metodo = ordenados.get(i);
            if(metodo != null && metodo.getModMetodo().getModificador().equals("dynamic")){ 
                sinMetodos = false;
                GeneradorDeCodigo.generar("DW " + metodo.definido().getNombre() + "_" + metodo.getNombre() + "\n");            
            }
        }
        if(sinMetodos)
            GeneradorDeCodigo.generar(" NOP\n");
        
    }
    
     private void chequeoHerenciaCircular(Clase clase, List<Clase> listPadres) throws ErrorSemantico {     
        if(listPadres.contains(clase)){
            throw new ErrorSemantico("Herencia circular. Linea:" + linea);
        }
        else
            if (!clase.getNombre().equals("Object")){
                listPadres.add(clase);   
                chequeoHerenciaCircular(clase.getPadre(), listPadres);
            }               
    }

    void chequeoSentencias() throws ErrorSemantico, IOException {
        //agregarlo en el orden de los offset!!!!
        for (Map.Entry<String, Metodo> entry : tablaMetodos.entrySet()) {
            Metodo metodo = entry.getValue();
            if(metodo.definido() == this){  
                //GeneradorDeCodigo.generar(".CODE \n");
                metodo.chequeoSentencias(this);
            }
        }        
        //GeneradorDeCodigo.generar(".CODE \n");
        constructor.chequeoSentencias(this);
    }

    @Override
    public boolean conforma(Tipo x) {
        Clase y = this;
        boolean conforma;
        if(y.nombre.getLexema().equals("Object"))
            conforma = true;
        else
            conforma = x.equals(y);
        while(!conforma && !y.getNombre().equals("Object")){
            y = y.getPadre();
            conforma = x.equals(y);
        }        
        return conforma;
    }

    @Override
    public boolean esPrimitivo() {
        return false;
    }

    public boolean isCompleta() {
        return completa;
    }

    public void setCompleta(boolean completa) {
        this.completa = completa;
    }
    
    public boolean contieneAtributo(String nombre){
        for(Atributo a : listaAtributos){
            if(a.getNombre().equals(nombre))
                return true;
        }
        return false;
    }
} 
